package net.stuffrepos.bricknetcenter.database;

import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import net.stuffrepos.bricknetcenter.database.HibernateUtil.Transaction;
import net.stuffrepos.bricknetcenter.database.schema.BlobInformation;
import net.stuffrepos.bricknetcenter.database.schema.HttpRequest;
import net.stuffrepos.bricknetcenter.database.schema.HttpRequestPostParameter;
import net.stuffrepos.bricknetcenter.database.schema.Information;
import net.stuffrepos.bricknetcenter.database.schema.Information.Type;
import net.stuffrepos.bricknetcenter.database.schema.Inventory;
import net.stuffrepos.bricknetcenter.database.schema.Transfer;
import net.stuffrepos.bricknetcenter.database.schema.TransferObject;
import net.stuffrepos.bricknetcenter.http.HttpRequest.PostParameter;
import net.stuffrepos.bricknetcenter.util.IoUtil;
import net.stuffrepos.bricknetcenter.util.LoopingThread;
import net.stuffrepos.bricknetcenter.util.TimedLoopingThread;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

/**
 *
 * @author Eduardo H. Bogoni <eduardobogoni@gmail.com>
 */
public class DataManager {

    private static final Log log = LogFactory.getLog(DataManager.class);
    private static final DataManager INSTANCE = new DataManager();
    private final EventManager eventManager = new EventManager();
    private final InformationManager informationManager = new InformationManager();
    private final HttpRequestManager httpRequestManager = new HttpRequestManager();

    // <editor-fold defaultstate="collapsed" desc="Main">
    private DataManager() {
    }

    public static DataManager getInstance() {
        return INSTANCE;
    }

    public InformationManager getInformationManager() {
        return informationManager;
    }

    public HttpRequestManager getHttpRequestManager() {
        return httpRequestManager;
    }

    private List findAll(final Class entityClass) throws Exception {
        return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<List>() {

            @Override
            public List execute(Session session) throws Exception {
                return session.createCriteria(entityClass).list();
            }
        });
    }

    public void addDataListener(DataListener listener) {
        eventManager.addDataListener(listener);
    }

    public void stop() {
        eventManager.stop();
        httpRequestManager.stop();
        informationManager.stop();
    }// </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Inventories">
    public synchronized Inventory findInventoryByName(final String name) throws Exception {
        return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Inventory>() {

            @Override
            public Inventory execute(Session session) throws Exception {
                return (Inventory) session.createCriteria(Inventory.class).add(Restrictions.eq("name", name)).uniqueResult();
            }
        });
    }

    public synchronized Inventory createNewInventory(String name, String comments) throws Exception {
        Inventory inventory = new Inventory();
        inventory.setName(name);
        inventory.setComments(comments);
        inventory.setCreatedOn(new Date());

        final Inventory result = DatabaseManager.getInstance().getHibernateUtil().save(inventory);

        eventManager.addAddedObject(result);
        eventManager.flush();

        return result;
    }

    public synchronized List<Inventory> findAllInventories() throws Exception {
        return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<List<Inventory>>() {

            @Override
            public List<Inventory> execute(Session session) throws Exception {
                return (List<Inventory>) session.createCriteria(Inventory.class).list();
            }
        });
    }

    public void updateInventory(final Inventory inventory) throws Exception {
        DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {

            @Override
            public Boolean execute(Session session) throws Exception {
                session.update(inventory);
                return true;
            }
        });

        eventManager.addUpdatedObject(inventory);
        eventManager.flush();
    }

    public void saveTransfer(final Transfer transfer) throws Exception {
        final boolean isNew = transfer.getId() == null;
        final Date now = new Date();

        if (isNew) {
            transfer.setCreatedOn(now);
        }
        transfer.setModifiedOn(now);
        DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {

            @Override
            public Boolean execute(Session session) throws Exception {
                for (TransferObject transferObject : transfer.getObjects()) {
                    transferObject.setTransfer(transfer);
                    if (transferObject.getId() == null) {
                        transferObject.setCreatedOn(now);
                    }
                    transferObject.setModifiedOn(now);
                }

                if (isNew) {
                    session.save(transfer);
                } else {
                    session.update(transfer);
                }

                return true;
            }
        });


        if (isNew) {
            eventManager.addAddedObject(transfer);
        } else {
            eventManager.addUpdatedObject(transfer);
        }
        eventManager.flush();
    }

    public void deleteTransfers(final Collection<Transfer> transfers) throws Exception {
        DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {

            @Override
            public Boolean execute(Session session) throws Exception {
                for (Transfer transfer : transfers) {
                    transfer.getInventory().getTransfers().remove(transfer);
                    session.delete(transfer);
                    eventManager.addDeletedObject(transfer);
                }
                return true;
            }
        });
        eventManager.flush();

    }

    public List<Transfer> findAllTransfers() throws Exception {
        return findAll(Transfer.class);
    }

    public List<TransferObject> findAllTransferObjects() throws Exception {
        return findAll(TransferObject.class);
    }

    public synchronized void deleteInventories(final Collection<Inventory> inventories) throws Exception {
        DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {

            @Override
            public Boolean execute(Session session) throws Exception {
                for (Inventory inventory : inventories) {
                    session.delete(inventory);
                    eventManager.addDeletedObject(inventory);
                }
                return true;
            }
        });
        eventManager.flush();
    }

    public synchronized int getInventoriesCount() throws Exception {
        return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Integer>() {

            @Override
            public Integer execute(Session session) throws Exception {
                return (Integer) session.createCriteria(Inventory.class).setProjection(Projections.rowCount()).uniqueResult();
            }
        });
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="HttpRequests">
    public class HttpRequestManager {

        private TimedLoopingThread flusherThread = new TimedLoopingThread(5000, new Runnable() {

            @Override
            public void run() {
                try {
                    newRequestsManager.flush();
                } catch (Exception ex) {
                    log.warn("Exception on flushing http requests", ex);
                }
            }
        });
        private final NewRequestsManager newRequestsManager = new NewRequestsManager();

        private HttpRequestManager() {
            flusherThread.start();
        }

        public void addHttpRequest(final net.stuffrepos.bricknetcenter.http.HttpRequest request) throws Exception {
            newRequestsManager.add(request);
        }

        public synchronized void markAsFailed(HttpRequest request) throws Exception {
            updateHttpRequestStatus(request, HttpRequest.Status.Failed);
        }

        public synchronized void resetHttpRequests() throws Exception {
            List<HttpRequest> requests = DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<List<HttpRequest>>() {

                @Override
                public List<HttpRequest> execute(Session session) throws Exception {
                    return session.createCriteria(HttpRequest.class).
                            add(Restrictions.eq("status", HttpRequest.Status.Requested)).
                            list();
                }
            });

            for (HttpRequest request : requests) {
                updateHttpRequestStatus(request, HttpRequest.Status.New);
            }
        }

        public synchronized HttpRequest getNextHttpRequest() throws Exception {
            HttpRequest request = DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<HttpRequest>() {

                @Override
                public HttpRequest execute(Session session) throws Exception {
                    HttpRequest request = (HttpRequest) session.createCriteria(HttpRequest.class).
                            add(Restrictions.or(Restrictions.eq("status", HttpRequest.Status.New), Restrictions.eq("status", HttpRequest.Status.Failed))).
                            addOrder(Order.desc("priority")).
                            addOrder(Order.asc("modifiedOn")).
                            setMaxResults(1).
                            uniqueResult();


                    return request;
                }
            });

            if (request != null) {
                updateHttpRequestStatus(request, HttpRequest.Status.Requested);
            }

            if (log.isTraceEnabled()) {
                log.trace(
                        String.format(
                        "Total: %d, New: %d, Requested: %d, Completed: %d, Failed: %d",
                        getHttpRequestCount(),
                        getHttpRequestCount(HttpRequest.Status.New),
                        getHttpRequestCount(HttpRequest.Status.Requested),
                        getHttpRequestCount(HttpRequest.Status.Completed),
                        getHttpRequestCount(HttpRequest.Status.Failed)));
            }

            return request;
        }

        private long getHttpRequestCount() throws Exception {
            return getHttpRequestCount(null);
        }

        private long getHttpRequestCount(final HttpRequest.Status status) throws Exception {
            return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Number>() {

                @Override
                public Number execute(Session session) throws Exception {
                    Criteria criteria = session.createCriteria(HttpRequest.class).setProjection(Projections.rowCount());
                    if (status != null) {
                        criteria.add(Restrictions.eq("status", status));
                    }
                    return (Number) criteria.uniqueResult();
                }
            }).longValue();
        }

        public synchronized void markAsCompleted(HttpRequest request) throws Exception {
            updateHttpRequestStatus(request, HttpRequest.Status.Completed);
        }

        public synchronized void updateHttpRequestStatus(final HttpRequest request, final HttpRequest.Status newStatus) throws Exception {
            assert request != null;
            assert newStatus != null;
            DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {

                @Override
                public Boolean execute(Session session) throws Exception {
                    request.setModifiedOn(new Date());
                    request.setStatus(newStatus);
                    session.update(request);
                    return true;
                }
            });
        }

        private boolean existsHttpRequest(final net.stuffrepos.bricknetcenter.http.HttpRequest request, Session session) throws Exception {
            assert request != null;
            assert session != null;
            Criteria c = session.createCriteria(HttpRequest.class).
                    add(Restrictions.eq("urlSpecification", request.getUrl().toString()));

            c.createAlias("postParameters", "pp");

            for (PostParameter pp : request.getPostParameters()) {
                c.add(Restrictions.eq("pp.name", pp.getName()));
                c.add(Restrictions.eq("pp.value", pp.getValue()));
            }

            return c.uniqueResult() != null;
        }

        private void stop() {
            try {
                newRequestsManager.flush();
            } catch (Exception ex) {
                log.warn("Exception on flushing new requests.");
            }
        }

        private class NewRequestsManager {

            private final List<net.stuffrepos.bricknetcenter.http.HttpRequest> newRequests = new LinkedList<net.stuffrepos.bricknetcenter.http.HttpRequest>();

            private synchronized void flush() throws Exception {
                log.debug("Flushing new http requests: " + newRequests.size());
                DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {

                    @Override
                    public Boolean execute(Session session) throws Exception {
                        Date now = new Date();
                        for (net.stuffrepos.bricknetcenter.http.HttpRequest request : newRequests) {
                            if (log.isTraceEnabled()) {
                                log.trace("Trying adding request with url " + request.getUrl());
                            }

                            if (existsHttpRequest(request, session)) {
                                if (log.isTraceEnabled()) {
                                    log.trace("Request with url " + request.getUrl() + " already exists.");
                                }
                            } else {
                                if (log.isTraceEnabled()) {
                                    log.trace("Adding \"" + request.getUrl() + "\"...");
                                }

                                HttpRequest persistenceRequest = new HttpRequest();
                                persistenceRequest.setHttpRequest(request);
                                persistenceRequest.setCreatedOn(now);
                                persistenceRequest.setModifiedOn(now);
                                persistenceRequest.setStatus(HttpRequest.Status.New);
                                session.save(persistenceRequest);
                            }

                        }
                        return true;
                    }
                });

                newRequests.clear();
            }

            private synchronized void add(net.stuffrepos.bricknetcenter.http.HttpRequest request) throws Exception {
                newRequests.add(request);
            }
        }
    }

// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Informations">
    public class InformationManager {

        private InformationManager() {
        }

        private InformationNode getInformationNode(Information parent, Type type, String value) throws Exception {

            Information information = getInformation(parent, type, value);

            if (information == null) {
                information = createInformation(parent, type, value);
            }

            return new InformationNode(information);
        }

        public InformationNode getInformationNode(Type type, String value) throws Exception {
            return getInformationNode(null, type, value);
        }

        private InformationNode getBlobInformationNode(Information parent, Type type,
                String value, String mimeType, InputStream in) throws Exception {

            BlobInformation information = getBlobInformation(parent, type, value);

            if (information == null) {
                information = createBlobInformation(parent, type, value, mimeType, in);
            }

            return new InformationNode(information);
        }

        private BlobInformation getBlobInformation(final Information parent, final Information.Type type, final String value) throws Exception {
            return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<BlobInformation>() {

                @Override
                public BlobInformation execute(Session session) throws Exception {
                    Criteria c = session.createCriteria(BlobInformation.class).
                            add(Restrictions.eq("type", type)).
                            add(Restrictions.eq("value", value));
                    if (parent == null) {
                        c.add(Restrictions.isNull("parent"));
                    } else {
                        c.add(Restrictions.eq("parent", parent));
                    }
                    return (BlobInformation) c.uniqueResult();
                }
            });
        }

        private Information createInformation(final Information parent, final Information.Type type, final String value) throws Exception {
            Information information = DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Information>() {
                @Override
                public Information execute(Session session) throws Exception {
                    Information information = new Information();
                    information.setType(type);
                    information.setValue(value);
                    information.setParent(parent);
                    /*
                    if (log.isTraceEnabled()) {
                    log.trace(String.format("Creating information \"%s\"", information.getDn()));
                    }*/
                    session.save(information);
                    return information;
                }
            });
            eventManager.addAddedObject(information);

            return information;
        }

        public List<Information> findInformationChildren(final Information parent) throws Exception {
            return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<List<Information>>() {

                @Override
                public List<Information> execute(Session session) throws Exception {
                    Criteria c = session.createCriteria(Information.class);
                    if (parent == null) {
                        c.add(Restrictions.isNull("parent"));
                    } else {
                        c.add(Restrictions.eq("parent", parent));
                    }
                    return (List<Information>) c.list();
                }
            });
        }

        public int getAllInformationsCount() throws Exception {
            return getInformationsCount(null);
        }

        public int getInformationsCount(final Information.Type type) throws Exception {
            return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Integer>() {

                @Override
                public Integer execute(Session session) throws Exception {
                    Criteria c = session.createCriteria(Information.class).setProjection(Projections.rowCount());
                    if (c != null) {
                        c.add(Restrictions.eq("type", type));
                    }
                    return ((Number) c.uniqueResult()).intValue();
                }
            });
        }

        private Information getInformation(final Information parent, final Type type, final String value) throws Exception {
            return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Information>() {

                @Override
                public Information execute(Session session) throws Exception {
                    Criteria c = session.createCriteria(Information.class).
                            add(Restrictions.eq("type", type)).
                            add(Restrictions.eq("value", value));
                    if (parent == null) {
                        c.add(Restrictions.isNull("parent"));
                    } else {
                        c.add(Restrictions.eq("parent", parent));
                    }
                    return (Information) c.uniqueResult();
                }
            });
        }

        public Iterator<Information> findAllInformations() throws Exception {
            return DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Iterator<Information>>() {

                @Override
                public Iterator<Information> execute(Session session) throws Exception {
                    return session.createQuery("from Information").iterate();
                }
            });
        }

        private BlobInformation createBlobInformation(final Information parent,
                final Type type, final String value, final String mimeType,
                final InputStream in) throws Exception {
            BlobInformation information = DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<BlobInformation>() {

                @Override
                public BlobInformation execute(Session session) throws Exception {
                    BlobInformation information = new BlobInformation();
                    information.setType(type);
                    information.setValue(value);
                    information.setParent(parent);
                    information.setMimeType(mimeType);
                    information.setContent(IoUtil.getBytesFromInputStream(in));

                    session.save(information);
                    return information;
                }
            });
            eventManager.addAddedObject(information);

            return information;
        }

        private void stop() {
            //Do nothing (For while)
        }

        public LazyResultSet<Information> findAllInformations(Information.Type informationType) throws Exception {
            assert informationType != null;
            return DatabaseManager.getInstance().getHibernateUtil().queryLazy(
                    new QueryData("from Information i where i.type = :type order by i.value").setParameter("type", informationType));
        }

        public LazyResultSet<Information> findAllInformations(Information.Type informationType, String value) throws Exception {
            assert informationType != null;
            return DatabaseManager.getInstance().getHibernateUtil().queryLazy(
                    new QueryData("from Information i where i.type = :type and lower(i.value) "
                    + "like lower('%' || :value || '%') order by i.value").setParameter("type", Information.Type.Set).
                    setParameter("value", value));
        }

        public void addInformationTree(final InformationNode rootNode, final InformationTree tree) throws Exception {
            final List<Information> added = new LinkedList<Information>();
            DatabaseManager.getInstance().getHibernateUtil().executeTransaction(new Transaction<Boolean>() {
                @Override
                public Boolean execute(Session session) throws Exception {
                    for (InformationTree treeChild : tree.getChildren()) {
                        addInformationTreeNoTransaction(rootNode.getInformation(), treeChild, session, added);
                    }
                    return true;
                }
            });
            for(Information information: added) {
                eventManager.addAddedObject(information);
            }
        }

        private void addInformationTreeNoTransaction(Information parent, InformationTree tree, Session session, List<Information> added) {
            Information information = new Information();
            information.setType(tree.getRootType());
            information.setValue(tree.getRootValue());
            information.setParent(parent);
            session.save(information);
            added.add(information);
            for (InformationTree treeChild : tree.getChildren()) {
                addInformationTreeNoTransaction(information, treeChild, session,added);
            }
        }

        public class InformationNode {

            private InformationNodeGroup children = new InformationNodeGroup();
            private final Information information;

            public InformationNode(
                    Information information) {
                this.information = information;
            }

            public InformationNode addChild(Type type, String value) throws Exception {
                return getInformationManager().getInformationNode(this.information, type, value);
            }

            public InformationNode addBlobChild(Type type, String value, String mimeType, InputStream in) throws Exception {
                return getInformationManager().getBlobInformationNode(this.information, type, value, mimeType, in);
            }

            public InformationNodeGroup getChildren() {
                return children;
            }

            private Information getInformation() {
                return information;
            }

            public class InformationNodeGroup {

                public InformationNodeGroup addNode(Type type, String value) throws Exception {
                    addChild(type, value);
                    return this;
                }

                public InformationNodeGroup addNode(Type type, int value) throws Exception {
                    return addNode(type, Integer.toString(value));
                }
            }
        }
    }
// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="Events">
    private class EventManager {

        private final FirerThread firerThread = new FirerThread();
        private final ListenerManager listenerManager = new ListenerManager();
        private final InternalDataEvent dataEvent = new InternalDataEvent();

        public EventManager() {
            firerThread.start();
        }

        private synchronized void addAddedObject(Object object) {
            dataEvent.addedObject(object);
        }

        private synchronized void flush() {

            if (dataEvent.hasObjects()) {
                if (log.isTraceEnabled()) {
                    log.trace(String.format("Flushing objects: %d added, %d: deleted, %d updated.",
                            dataEvent.getAddedObjects().size(),
                            dataEvent.getDeletedObjects().size(),
                            dataEvent.getUpdatedObjects().size()));
                }

                try {
                    listenerManager.fireDataEvent(dataEvent);
                } catch (Exception ex) {
                    log.warn("Exception on firing data event", ex);
                }

                dataEvent.clear();

            }
        }

        private void addUpdatedObject(Object object) {
            dataEvent.updateObject(object);
        }

        private void addDeletedObject(Object object) {
            dataEvent.deletedObject(object);
        }

        private void addDataListener(DataListener listener) {
            listenerManager.addDataListener(listener);
        }

        private void stop() {
            firerThread.stopLooping();
        }

        private class InternalDataEvent implements DataEvent {

            private List<Object> addedObjects = new LinkedList<Object>();
            private List<Object> deletedObjects = new LinkedList<Object>();
            private List<Object> updatedObjects = new LinkedList<Object>();

            public void addedObject(Object object) {
                addedObjects.add(object);
            }

            @Override
            public Collection<Object> getAddedObjects() {
                return addedObjects;
            }

            public void deletedObject(Object object) {
                deletedObjects.add(object);
            }

            @Override
            public Collection<Object> getDeletedObjects() {
                return deletedObjects;
            }

            private void updateObject(Object object) {
                updatedObjects.add(object);
            }

            private void clear() {
                updatedObjects.clear();
                addedObjects.clear();
                deletedObjects.clear();
            }

            private boolean hasObjects() {
                return !addedObjects.isEmpty() || !deletedObjects.isEmpty()
                        || !updatedObjects.isEmpty();
            }

            private Collection<Object> getUpdatedObjects() {
                return updatedObjects;
            }
        }

        private class ListenerManager {

            private final List<DataListener> listeners = new LinkedList<DataListener>();

            public synchronized void addDataListener(DataListener listener) {
                listeners.add(listener);
            }

            private synchronized void fireDataEvent(final InternalDataEvent e) {
                log.debug("Firing data event (Listeners: " + listeners.size() + ")");
                for (DataListener listener : listeners) {
                    listener.dataChanged(e);
                }
            }
        }

        private class FirerThread extends LoopingThread {

            @Override
            public void loopingRun() {
                try {
                    flush();
                } catch (Exception ex) {
                    log.fatal("Exception on firing data event", ex);
                }
                try {
                    sleep(1000);
                } catch (InterruptedException ex) {
                    log.warn("", ex);
                }
            }
        }
    }// </editor-fold>
}
